import copy
import datetime
import json

import arrow

from sqlalchemy import func, orm, sql
from sqlalchemy import (
    Integer,
    Float,
    Boolean,
    String,
    Text,
    Date,
    DateTime,
    Time,
    ARRAY,
)

from .exceptions import *
from .sqltypes import *


# ----------------------------------------------------------
#
#                           模型基类
#
# ----------------------------------------------------------


class Base(orm.declarative_base()):
    __abstract__ = True

    def __getattribute__(self, name):
        attribute = object.__getattribute__(self, name)

        if isinstance(attribute, datetime.datetime):
            attribute = arrow.get(attribute).to("utc").datetime

        return attribute

    def __setattr__(self, name, value):
        if name in self.__table__.c.keys():
            c = self.__table__.c.get(name)

            if isinstance(value, datetime.datetime):
                value = arrow.get(value).to("utc").datetime
            elif isinstance(value, str):
                if isinstance(c.type, String):
                    if c.type.length:
                        value = value[: c.type.length]
            else:
                pass

        super().__setattr__(name, value)

    def set_choice(self, name, choice):
        if name in self.__table__.c.keys():
            c = self.__table__.c.get(name)

            if isinstance(c.type, ChoiceType):
                setattr(self, name, c.type.make(choice))

    def dict(self, excludes=None, exclude_none=True):
        data = {}

        for k in self.__table__.c.keys():
            if excludes:
                if k in excludes:
                    continue

            obj = getattr(self, k)

            if obj is None:
                if not exclude_none:
                    data[k] = None

                continue

            c = self.__table__.c.get(k)

            if isinstance(c.type, ChoiceType):
                data[k] = obj.__dict__

                continue

            if isinstance(c.type, CoordinateType):
                data[k] = (obj.x, obj.y)

                continue

            if isinstance(c.type, ARRAY):
                if isinstance(c.type.item_type, ChoiceType):
                    choices = []

                    for choice in obj:
                        choices.append(choice.__dict__)

                    data[k] = choices

                    continue

            data[k] = obj

        return data

    def from_data(self, data, excludes=None, exclude_none=False, session=None):
        if not session:
            session = self._sa_instance_state.session

        with session.no_autoflush:
            map = {}

            for model in self.__class__.__base__.__subclasses__():
                map[model.__tablename__] = model

            for k, v in data.items():
                if k == "id":
                    continue

                if excludes:
                    if k in excludes:
                        continue

                if k not in self.__table__.c.keys():
                    k = f"{k}_id"

                    if k not in self.__table__.c.keys():
                        continue

                c = self.__table__.c.get(k)

                if c.foreign_keys:
                    key = k[:-3]

                    if v is None:
                        if not exclude_none:
                            setattr(self, key, None)

                        continue

                    if isinstance(v, int):
                        model = map.get(list(c.foreign_keys)[0].target_fullname[:-3])

                        if model:
                            obj = session.get(model, v)

                            if not obj:
                                description = (
                                    (model.__doc__ or "").strip().splitlines()[0]
                                )

                                raise HTTPException(
                                    detail=f"未知{description}",
                                    status_code=HTTP_400_BAD_REQUEST,
                                )

                            setattr(self, key, obj)
                else:
                    if v is None:
                        if not exclude_none:
                            setattr(self, k, None)

                        continue

                    if isinstance(c.type, ChoiceType | CoordinateType | PasswordType):
                        setattr(self, k, c.type.make(v))

                        continue

                    if isinstance(c.type, ARRAY):
                        if isinstance(c.type.item_type, ChoiceType):
                            choices = []

                            for choice in v:
                                choices.append(c.type.item_type.make(choice))

                            setattr(self, k, choices)

                            continue

                    if isinstance(v, str):
                        v = v.strip()

                    setattr(self, k, v)

    def export(self, keys):
        data = []

        for k in keys:
            if not hasattr(self, k):
                data.append(None)

                continue

            value = getattr(self, k)

            if value is None:
                data.append(None)

                continue

            if isinstance(value, Choice):
                data.append(value)
            elif isinstance(value, Password):
                data.append("")
            elif isinstance(value, Coordinate):
                data.append(f"({value.x},{value.y})")
            elif isinstance(value, Base):
                if hasattr(value, "name"):
                    data.append(value.name)
                else:
                    data.append(value)
            elif isinstance(value, list):
                dup = []

                for x in value:
                    if isinstance(x, Choice):
                        x = x.value

                    dup.append(str(x))

                data.append("\n".join(dup))
            else:
                data.append(value)

        return data

    def dirty(self, data, excludes=None):
        dirty = False

        for k, v in data.items():
            if excludes:
                if k in excludes:
                    continue

            if k in self.__table__.c:
                if v != getattr(self, k):
                    dirty = True

                    break

        return dirty


class JSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return arrow.get(obj).to("utc").format()
        elif isinstance(obj, (datetime.date, datetime.time)):
            return obj.isoformat()
        elif isinstance(obj, (geoalchemy2.WKBElement, geoalchemy2.WKTElement)):
            return str(obj)
        else:
            return json.JSONEncoder.default(self, obj)


class SessionClass(orm.Session):
    def count(self, stmt):
        stmt = copy.copy(stmt.with_only_columns(func.count(stmt.selected_columns[0])))
        stmt._order_by_clauses = ()

        return self.execute(stmt).scalar()


# ----------------------------------------------------------
#
#                           辅助方法
#
# ----------------------------------------------------------


def compute_distance_func(col, coordinate, name="distance"):
    if isinstance(col, orm.InstrumentedAttribute):
        coordinate_col = col
    else:
        if col is not None:
            coordinate_col = col.wk_text()
        else:
            coordinate_col = None

    coordinate_obj = CoordinateType().make(coordinate)

    if coordinate_obj is not None:
        coordinate_text = coordinate_obj.wk_text()
    else:
        coordinate_text = None

    return func.ST_DistanceSphere(
        coordinate_col, func.ST_GeomFromText(coordinate_text)
    ).label(name)


def queryset_distance_range(stmt, col, coordinate, range=1000, exclude_none=True):
    coordinate_obj = CoordinateType().make(coordinate)

    if coordinate_obj is not None:
        coordinate_text = coordinate_obj.wk_text()
    else:
        coordinate_text = None

    if exclude_none:
        stmt = stmt.where(func.ST_DistanceSphere(col, coordinate_text) <= range)
    else:
        stmt = stmt.where(
            sql.or_(
                col.is_(None),
                func.ST_DistanceSphere(col, coordinate_text) <= range,
            )
        )

    return stmt


def humanize_distance(distance):
    distance = int(distance)

    if distance > 1000:
        return f"{round(distance/1000, 2)}公里".replace(".0", "")
    else:
        return f"{distance}米"


def utcnow():
    return arrow.utcnow().datetime
